STATS.NBA.COM 크롤링(스크래핑) by using JSON in Python
STATS.NBA.COM 파싱하기 드러워서 내가 쓴다…
크롤링인지 스크래핑인지 만만치 않다는 사실을 깨달았읍니다…
1 | Question: 아래 URL의 NBA 데이터를 크롤링하여 판다스 데이터 프레임으로 나타내세요. |
해당 페이지 링크는 아래과 같다.
http://stats.nba.com/teams/traditional/?sort=GP&dir=-1
그리고 우리가 Pandas로 구현해야 하는
이 테이블을 JSON 파일 형태로 html Pasing하여 출력하도록 하겠다.
1. STATS.NBA.COM 해당 페이지의 숨어있는 JSON 파일 찾기
위에 표시된 링크에 들어가면 이러한 화면이 보인다.
이곳에서 키보드의 F12
를 누르면, 오른쪽에 개발자 도구가 뜨게 되는데, Network
에 들어가서 XHR
탭을 클릭하고 F5
를 눌러보자.
Name
탭에 5가지의 Request 결과가 뜨는데, 이 중 4번째인 leaguedashteamstats?Conference=
로 시작하는 데이터를 클릭하고 오른쪽 탭의 resultSets
를 계속 펼쳐보면 rowSet
묶음에 STATS.NBA.COM 페이지에서 봤던 익숙한 NBA 팀 이름들이 뜬다.
개발자 도구 안에서도 JSON의 구조를 파악하고 Pasing이 가능하지만, 우리는 JSON 파일을 예쁘게 볼 수 있는 크롬 앱을 사용해보도록 하겠다. 필자가 쓰는 JSON Viewer는 Json Viewer다. 이 곳에서 크롬 앱에 추가할 수 있다. 일단 추가하면 JSON 파일을 열 때 알아서 예쁘게 보여준다.
그럼 이제 leaguedashteamstats?어쩌구를 더블클릭해서 열어보자… 혹은 이 링크를 누르면 JSON 형태의 웹 페이지가 보인다.
이것이 바로 우리가 데이터를 뽑아내야 할 JSON 웹 페이지의 구조이다. 오른쪽 위에 보이는 위아래 화살표 버튼을 클릭하면
이런 식으로 접히게 된다. 정신산란할 땐 접어놓고 복습호흡을 내뱉으며 하나하나 뜯어가며 코드를 분해하도록 하자.
이정도 정보를 얻었으면, 코드를 조금 작성해보자. IDE는 Jupyter Notebook을 사용하였다.
1 | import requests |
import할 패키지는 총 3가지이다.
- 원하는 웹 페이지에 Request(요청)를 보내 html 결과를 받기 위한
requests
- html 안의 text 결과를 받아와서 데이터로 사용하기 위한
json
- 그리고 DataFrame으로 데이터를 테이블 형태로 만들어줄
pandas
.as pd
를 붙이는 이유는 pandas를 매번 입력하기 귀찮아서 pd로 퉁쳐주기 위한 것이다.
1 | url = "http://stats.nba.com/stats/leaguedashteamstats?Conference=&DateFrom=&DateTo=&Division=&GameScope=&GameSegment=&LastNGames=0&LeagueID=00&Location=&MeasureType=Base&Month=0&OpponentTeamID=0&Outcome=&PORound=0&PaceAdjust=N&PerMode=PerGame&Period=0&PlayerExperience=&PlayerPosition=&PlusMinus=N&Rank=N&Season=2017-18&SeasonSegment=&SeasonType=Playoffs&ShotClockRange=&StarterBench=&TeamID=0&VsConference=&VsDivision=" |
우선 url 변수에 JSON 파일의 url 주소를 입력해준다. 그리고 아래 get_data 함수의 Parameter로 써준다.
1 | def get_data(url): |
하나씩 뜯어가면서 설명하겠다. 아래 코드는 위의 코드와 중복된다. 그러니 위의 코드만 입력하면 된다. 그냥 설명용으로 작성한다.
1 | headers = {'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64)\ |
STATS.NBA.COM은 아마도 Scraping을 하기 위해 필요로 하는 웹 브라우저 버전이 따로 있는 것 같다. 필자는 Chrome 67.0.3396.87 64Bit 버전을 사용하는데, 61.0.3163.100 버전을 사용하는 것 처럼 속여줘야 데이터를 가져오는 듯 하다. 그냥 구글링 해서 나의 고통을 가지고 있는 분의 질문에 대한 해답에 이런 늬앙스의 글이 있었다. 일단 headers 변수에 해당 내용을 입력해주자.
1 | response = requests.get(url, headers=headers) |
response 변수에 아까 입력한 url 주소의 html을 get 방식으로 받겠다고 요청을 보낸다. json_info 변수에 response변수로 받은 html내용의 text를 load하겠다는 명령어를 입력해준다.
1 | stats = json_info["resultSets"][0]["rowSet"] |
가장 고생을 많이 한 부분이다. stats 변수에 rowSet의 데이터가 입력되어야 하는데(NBA 16팀), json_info["resultSets"]["rowSet"]
으로 썼더니 'str' object has no attribute 'read'
에러가 떴다. [0]을 중간에 추가해줘야한다. JSON Viewer로 보면 이 구조가 잘 안보이고, 오히려 개발자 도구에서 본 구조에서 힌트를 얻었다.
1 | df = pd.DataFrame(columns=[\ |
df 변수 주소값에 pandas DataFrame을 할당해준다. 상단 columns에 TEAM, GP, W 등의 변수들을 추가해준다. 그럼 아래와 같은 형태로 나타난다.
1 | for stat in stats: |
for문으로 각 행에 column과 일치하는 데이터를 순서대로 넣어준다. 나머지는 다 순서대로인데, PTS
탭만 stat[26] 데이터를 차용한다. 헷갈리지 말자. 여기까지 입력했으면 아래와 같이 df 변수의 DataFrame이 만들어진다.
1 | df = df.sort_values(by=["GP"], ascending=False) |
만들어진 df를 GP
column 기준으로 정렬해주고, ascending=False
명령어로 내림차순을 적용시켜준다. 그럼 아래와 같이 인덱스값이 흐트러지게 된다.
df = df.reset_index(drop=True)
명령으로 인덱스를 리셋해주고, 기존 인덱스는 drop해버린다. 더하여 인덱스가 0으로 시작하는데, df.index += 1
명령으로 인덱스에 모든 값을 1씩 더해준다. indent 위치를 주의하자. 이전 명령들의 indent보다 space 4칸이 적다. 그래야 모든 index에 적용된다. 마지막으로 df를 return해준다.
결과는…
위와 같다.
홈페이지의 이 표와 비교하면 내용은 완벽히 같다.. 추후 STATS.NBA.COM의 JSON url 값이나 column 고유값이 변하지 않는 이상, 팀간 GP 값의 변동이 있더라도 get_data 함수가 알아서 Parsing 해줄 것이다.
초기에 http://stats.nba.com/teams/traditional/?sort=GP&dir=-1 URL에서 보듯이, 이 예제에서는 GP값을 기준으로 정렬했는데 df = df.sort_values(by=["GP"], ascending=False)
명령에서 "GP"
부분을 다른 column 값으로 수정하면 다른 기준으로 정렬 될 것이다.
스크래핑, 혹은 크롤링이라고 불리는 작업이 쉽지 않다는 것을 깨달은 하루였다… T-T
소요 시간은 약 4시간정도. 그리고 이 글을 적는 것은 1시간 반 정도 걸렸다.ㅋㅋㅋㅋ
무려 5시간 넘게 뺏어간 놈이다.. 하지만 추후 크롤링을 더 수월하게 할 수 있을 것이라는 자신감이 생겼다.
다음 글 예고
- Selenium을 이용한 네이버 기사 10페이지의 제목 리스트 크롤링!